// Copyright (c) Appeon. All rights reserved.
// Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. 
// You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0.

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Microsoft.Extensions.DependencyInjection;

namespace SnapObjects.Expressions
{
    internal class EvaluatePolicyProvider : IPolicyProvider<EvaluateResult>
    {
        protected readonly EvaluateMethodMatcher _matcher;
        protected readonly EvaluateTypeSynchronizer _synchronizer;
        protected readonly IMetadataManager<EvaluateResult> _metadatas;
        protected readonly NestParseProvider<EvaluateResult> _nestParsers;

        public EvaluatePolicyProvider(IServiceProvider provider)
        {
            _synchronizer = new EvaluateTypeSynchronizer();
            _matcher = provider.GetService<EvaluateMethodMatcher>();
            _metadatas = provider.GetService<IMetadataManager<EvaluateResult>>();
            _nestParsers = provider.GetService<NestParseProvider<EvaluateResult>>();

            this.NestParser = new NestParseProvider<EvaluateResult>(provider);
        }

        public bool SupportShortCircuit => true;
        public INestParseProvider<EvaluateResult> NestParser { get; }

        public bool ShortCircuit(EvaluateResult test)
        {
            return test.Value is bool value && value;
        }

        public virtual EvaluateResult Addition(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.Add(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public virtual EvaluateResult Subtraction(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.Subtract(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public virtual EvaluateResult Multiply(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.Multiply(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public virtual EvaluateResult Division(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.Division(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public virtual EvaluateResult Modulus(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.Modulus(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public virtual EvaluateResult Caret(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.Caret(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult Equal(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.Equal(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult NotEqual(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.NotEqual(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult LessThan(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.LessThan(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult GreaterThan(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.GreaterThan(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult LessThanOrEqual(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.LessThanOrEqual(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult GreaterThanOrEqual(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.GreaterThanOrEqual(
                left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult Not(EvaluateResult body)
        {
            return new EvaluateResult(!(bool)body.Value, typeof(bool));
        }

        public EvaluateResult Negative(EvaluateResult body)
        {
            return new EvaluateResult(-(dynamic)body.Value, body.Type);
        }

        public EvaluateResult OrElse(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.OrElse(left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult AndAlso(
            EvaluateResult left, EvaluateResult right, ParserOptions<EvaluateResult> options)
        {
            var result = ObjectOperator.AndAlso(left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult And(EvaluateResult left, EvaluateResult right)
        {
            var result = ObjectOperator.And(left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult Or(EvaluateResult left, EvaluateResult right)
        {
            var result = ObjectOperator.Or(left.Value, left.Type, right.Value, right.Type);

            return new EvaluateResult(result.Value, result.Type);
        }

        public EvaluateResult Condition(EvaluateResult test, EvaluateResult ifTrue, EvaluateResult ifFalse)
        {
            return test.Value is bool value && value ? ifTrue : ifFalse;
        }

        public EvaluateResult List(IEnumerable<EvaluateResult> args)
        {
            return new EvaluateResult(args);
        }

        public EvaluateResult Constant(object value)
        {
            return new EvaluateResult(value);
        }

        public EvaluateResult Constant(object value, Type type)
        {
            return new EvaluateResult(Convert.ChangeType(value, type), type);
        }

        public EvaluateResult Identifier(string identifier, ParserOptions<EvaluateResult> options)
        {
            if (options is EvaluateParserOptions context)
            {
                var isOk = _metadatas.TryGetMetadata(
                    MetadataType.Property, identifier, options, out var metadata);

                if (isOk && metadata is PropertyMetadata<EvaluateResult> property)
                {
                    return property.Value;
                }

                isOk = _metadatas.TryGetMetadata(
                    MetadataType.Parameter, identifier, options, out var meta);

                if (isOk && meta is ParameterMetadata<EvaluateResult> parameter)
                {
                    return parameter.Value;
                }

                throw new IdentifierNotSupportedException(identifier);
            }

            return null;
        }

        public bool TryInvokeMethod(
            string name,
            EvaluateResult args,
            ParserOptions<EvaluateResult> options,
            out EvaluateResult result)
        {
            result = null;

            var isOk = _metadatas.TryGetMetadata(
                MetadataType.Method, name, options, out var metadata);

            if (isOk &&
                options is EvaluateParserOptions context &&
                metadata is EvaluateMethodMetadata methods)
            {
                var overloads = methods.OverLoads;

                if (args == null)
                {
                    var method = overloads.FirstOrDefault(x => !x.Parameters.Any());

                    if (method != null && method.Body.Value is MethodInfo methodInfo)
                    {
                        var value = methodInfo.Invoke(context.DataSource, null);

                        result = new EvaluateResult(value, methodInfo.ReturnType);

                        return true;
                    }
                }

                if (args.Value is IEnumerable arr)
                {
                    var outArgs = new List<EvaluateResult>();
                    var argus = arr.Cast<EvaluateResult>().ToArray();

                    var method = overloads.FirstOrDefault(
                        x => _matcher.MatchMethod(context, x, argus, out outArgs));

                    if (method != null && method.Body.Value is MethodInfo methodInfo)
                    {
                        var arguments = outArgs.Select(x => x.Value);

                        try
                        {
                            var value = methodInfo.Invoke(null, arguments.ToArray());

                            result = new EvaluateResult(value, methodInfo.ReturnType);

                            return true;
                        }
                        catch (Exception)
                        {
                            return false;
                        }
                    }
                }
            }

            return false;
        }
    }
}
